package erfgame.core.world.terrain.update;

import java.awt.Point;
import java.util.logging.Level;
import java.util.logging.Logger;


import erfgame.core.Point3D;
import erfgame.core.Rectangle3D;
import erfgame.core.dao.ObjectDAO;
import erfgame.core.memory.Pointer;
import erfgame.core.util.RangeUtils;
import erfgame.core.world.World;
import erfgame.core.world.terrain.TerrainUpdate;

public class TerrainBlockUpdater {

	private static final Logger log = Logger.getLogger( TerrainBlockUpdater.class.getName() );
	
	private int sampleBuffer;
	
	public TerrainBlockUpdater( int sampleBuffer ) {
		this.sampleBuffer = sampleBuffer;
	}
	
	public void loadShadowBlocks(
			World world, 
			TerrainUpdate terrainUpdate, 
			TerrainBlockUpdateResult into, 
			int worldX, 
			int worldY, 
			int worldZ,
			ObjectDAO<Point3D, Pointer<byte[][][]>> terrainDAO
	) {
		// get additional blocks for the shadowed area if required
		int maxz = into.getLowestUnderTerrainZ(); 
		Rectangle3D updateCoverage = into.getCoverage();
		if( maxz > updateCoverage.maxZ ) {
			int minz = into.getHighestUnderTerrainZ();

			int blockDepth = world.getBlockDepth();

			Rectangle3D sampleBounds = into.getSampleBounds();
			
			int startSampleBlockX = sampleBounds.minX;
			int startSampleBlockY = sampleBounds.minY;
			int startSampleBlockZ = Math.max( 0, ((minz - sampleBuffer)/blockDepth)-1 );
			
			int finalSampleBlockX = sampleBounds.maxX;
			int finalSampleBlockY = sampleBounds.maxY;
			int finalSampleBlockZ = Math.min( world.getBlocksZ()-1, (maxz + sampleBuffer - 1)/blockDepth );

			int sampleBlocksX = finalSampleBlockX - startSampleBlockX + 1;
			int sampleBlocksY = finalSampleBlockY - startSampleBlockY + 1;
			int sampleBlocksZ = finalSampleBlockZ - startSampleBlockZ + 1;
			
			// need to get a buffer around the blocks
			byte[][][][][][] allTerrain = new byte[sampleBlocksX][sampleBlocksY][sampleBlocksZ][][][];
			@SuppressWarnings("unchecked")
			Pointer<byte[][][]>[][][] pointers = new Pointer[sampleBlocksX][sampleBlocksY][sampleBlocksZ];
			
			for( int bx=sampleBlocksX; bx>0; ) {
				bx--;
				byte[][][][][] allTerrainX = allTerrain[bx];
				int worldBlockX = (bx+startSampleBlockX);
				for( int by=sampleBlocksY; by>0; ) {
					by--;
					byte[][][][] allTerrainXY = allTerrainX[by];
					int worldBlockY = (by+startSampleBlockY);
					for( int bz=sampleBlocksZ; bz>0; ) {
						bz--;
						Point3D key = new Point3D( worldBlockX, worldBlockY, bz + startSampleBlockZ );
						Pointer<byte[][][]> pointer = terrainDAO.retrieveObject(key); 
						allTerrainXY[bz] = pointer.getValue();
						pointers[bx][by][bz] = pointer;
					}
				}
			}
			into.setShadowPointers(pointers);
			into.setShadowSample(allTerrain, startSampleBlockZ, finalSampleBlockZ);
		}
		// otherwise do nothing, the shadow doesn't require additional blocks (either there is no shadow, or it it's part of the update (on a flat surface))
	}
	
	public TerrainBlockUpdateResult updateTerrainBlocks(
			World world,
			TerrainUpdate terrainUpdate, 
			int worldX, 
			int worldY, 
			int worldZ, 
			ObjectDAO<Point3D, Pointer<byte[][][]>> terrainDAO, 
			ObjectDAO<Point, int[][][]> surfacesDAO
	) {
		int originalWorldZ = worldZ;
		if( worldZ < 0 ) {
			worldZ = 0;
		}
		
		int terrainWidth = terrainUpdate.getWidth();
		int terrainHeight = terrainUpdate.getHeight();
		int terrainDepth = terrainUpdate.getDepth();
		short[][][] terrainToSet = terrainUpdate.getTerrain();
		int[][][] terrainSummary = terrainUpdate.getSummary();
		
		int blockWidth = world.getBlockWidth();
		int blockHeight = world.getBlockHeight();
		int blockDepth = world.getBlockDepth();
		
//		System.out.println( "("+worldX+","+worldY+","+worldZ+","+terrainWidth+","+terrainHeight+","+terrainDepth+")" );
		int startBlockX = Math.max( 0, worldX >> world.getBlockWidthShift() );
		int startBlockY = Math.max( 0, worldY >> world.getBlockHeightShift() );
		int startBlockZ = Math.max( 0, worldZ / blockDepth );
		int finalBlockX = Math.min( world.getBlocksX()-1, (worldX + terrainWidth - 1)>>world.getBlockWidthShift() );
		int finalBlockY = Math.min( world.getBlocksY()-1, (worldY + terrainHeight - 1)>>world.getBlockHeightShift() );
		int finalBlockZ = Math.min( world.getBlocksZ()-1, (worldZ + terrainDepth - 1)/blockDepth );

		log.log( Level.INFO, "overwrite terrain ("+startBlockX+","+startBlockY+","+startBlockZ+")->("+finalBlockX+","+finalBlockY+","+finalBlockZ+")" );

		int startSampleBlockX = Math.max( 0, (worldX - sampleBuffer)/blockWidth );
		int startSampleBlockY = Math.max( 0, (worldY - sampleBuffer)/blockHeight );
		// special case, always have a block above up your sleeve due to "looking down" errors
		// TODO : is there a way to calculate whether this will be needed?
		int startSampleBlockZ = Math.max( 0, ((worldZ - sampleBuffer)/blockDepth)-1 );
		
		int finalSampleBlockX = Math.min( world.getBlocksX()-1, (worldX + terrainWidth + sampleBuffer - 1 )/blockWidth );
		int finalSampleBlockY = Math.min( world.getBlocksY()-1, (worldY + terrainHeight + sampleBuffer - 1)/blockHeight );
		int finalSampleBlockZ = Math.min( world.getBlocksZ()-1, (worldZ + terrainDepth + sampleBuffer - 1)/blockDepth );

		int sampleBlocksX = finalSampleBlockX - startSampleBlockX + 1;
		int sampleBlocksY = finalSampleBlockY - startSampleBlockY + 1;
		int sampleBlocksZ = finalSampleBlockZ - startSampleBlockZ + 1;
		
		// need to get a buffer around the blocks
		byte[][][][][][] allTerrain = new byte[sampleBlocksX][sampleBlocksY][sampleBlocksZ][][][];
		int[][][][][] surfaces = new int[sampleBlocksX][sampleBlocksY][][][];
		Rectangle3D coverage = new Rectangle3D( 
				worldX, 
				worldY, 
				worldZ, 
				worldX + terrainWidth - 1, 
				worldY + terrainHeight - 1, 
				worldZ + terrainDepth - 1
		);
		
		int blockOffsetZ = startBlockZ - startSampleBlockZ;
		
		@SuppressWarnings("unchecked")
		Pointer<byte[][][]>[][][] pointers = new Pointer[sampleBlocksX][sampleBlocksY][sampleBlocksZ];
		
		log.log( Level.INFO, "setting terrain" );
		
		int startz = worldZ % world.getBlockDepth();
		//int startbz = worldZ / world.getBlockDepth();

		for( int bx=sampleBlocksX; bx>0; ) {
			bx--;
			byte[][][][][] allTerrainX = allTerrain[bx];
			int[][][][] surfacesX = surfaces[bx];
			int worldBlockX = (bx+startSampleBlockX);
			int blockX = worldBlockX * world.getBlockWidth();
			int[] xintersection = RangeUtils.intersection(
					worldX, 
					worldX+terrainWidth, 
					blockX, 
					blockX + world.getBlockWidth() 
			);
			int startttsx = xintersection[0] - worldX;
			int startx = xintersection[0] - blockX;
			int endx = xintersection[1] - blockX - 1;
			for( int by=sampleBlocksY; by>0; ) {
				by--;
				byte[][][][] allTerrainXY = allTerrainX[by];
				int worldBlockY = (by+startSampleBlockY);
				int blockY = worldBlockY * blockHeight;
				
				int[] yintersection = RangeUtils.intersection(
						worldY, 
						worldY+terrainHeight, 
						blockY, 
						blockY + blockHeight 
				);
				int startttsy = yintersection[0] - worldY;
				int starty  = yintersection[0] - blockY;
				int endy = yintersection[1] - blockY - 1; 

				
				for( int bz=sampleBlocksZ; bz>0; ) {
					bz--;
					Point3D key = new Point3D( worldBlockX, worldBlockY, bz + startSampleBlockZ );
					Pointer<byte[][][]> pointer = terrainDAO.retrieveObject(key); 
					allTerrainXY[bz] = pointer.getValue();
					pointers[bx][by][bz] = pointer;
				}
				Point surfaceKey = new Point( worldBlockX, worldBlockY );
				surfacesX[by] = surfacesDAO.retrieveObject(surfaceKey);
				// all loaded now...integrate changes
				
				if( startttsy >= 0 && startttsx >= 0 ) {
//					System.out.println( "("+bx+","+by+")/("+(bx+startBlockX)+","+(by+startBlockY)+")" );
					
					int ttsx = startttsx;
					for( int tx=startx; tx<=endx; tx++ ) {
						int[][] terrainSummaryX = terrainSummary[ttsx];
						short[][] terrainToSetX = terrainToSet[ttsx];
						ttsx++;
						int ttsy = startttsy;
						yloop: for( int ty=starty; ty<=endy; ty++ ) {
//							System.out.println( "-->("+tx+","+ty+")/("+(ttsx-1)+","+ttsy+")/("+endx+","+endy+")" );
							int[] terrainSummaryXY = terrainSummaryX[ttsy];
							short[] terrainToSetXY = terrainToSetX[ttsy];
							ttsy++;
							int tz = startz;
							int bz = blockOffsetZ;
							byte[] terrainXY = allTerrainXY[bz][tx][ty];
							int ttsz = 0;				
							int terrainSummaryIndex = 0;
							int count = terrainSummaryXY[terrainSummaryIndex];
							// work through any negative values
							if( originalWorldZ < 0 ) {
								int overlap = originalWorldZ;
								// TODO : this could be done by minusing stuff instead of one at a time
								ttsz = -overlap;
								while(overlap < 0 ) {
									count--;
									overlap++;
									if( count <= 0 ) {
										terrainSummaryIndex++;
										count = terrainSummaryXY[terrainSummaryIndex];
									}
								}
							}
							boolean done = false;
							while( !done ) {
								short terrainType = terrainToSetXY[ttsz];
								// TODO : this isn't correct behavior, should check the flag, not just air
								boolean skip = terrainType == TerrainUpdate.FLAG_OVERWRITE_IF_AIR; 
								while( count > 0 ) {
									int remainingLength = (blockDepth - tz);
									byte[] target = terrainXY;
									int targetIndex = tz;
									int length;
									if( count >= remainingLength ) {
										length = remainingLength;
										count -= remainingLength;
										bz++;
										if( bz < allTerrainXY.length ) {
											terrainXY = allTerrainXY[bz][tx][ty];
										} else {
//											terrainXY = null;
											// skip the rest
											continue yloop;
										}
										tz = 0;
									} else {
										length = count;
										tz+= count;
										count = 0;
									}
									if( !skip ) {
										// TODO this is slow! should use array copy! need to stop using shorts to do so?
										//System.arraycopy( terrainToSetXY, ttsz, target, targetIndex, length );
										for( int k=length; k>0; ) {
											k--;
											if( k+targetIndex < 0 || k+ttsz < 0 ) {
												log.info("we're fucked");
											}
											target[k+targetIndex] = (byte)terrainToSetXY[k+ttsz];
										}
									}
									ttsz+= length;
								}
								terrainSummaryIndex++;
								if( terrainSummaryIndex < terrainSummaryXY.length ) {
									count = terrainSummaryXY[terrainSummaryIndex];
								} else {
									done = true;
								}
							}
						}
					}
					for( int bz=sampleBlocksZ; bz>0; ) {
						bz--;
						Point3D key = new Point3D( bx + startSampleBlockX, by + startSampleBlockY, bz + startSampleBlockZ );
						// TODO : only store if it has changed on bz!!!
						terrainDAO.storeObject(key, pointers[bx][by][bz]);
					}
					
				}
			}
		}
		return new TerrainBlockUpdateResult(
				new Rectangle3D( 
						startSampleBlockX, 
						startSampleBlockY,
						startSampleBlockZ, 
						finalSampleBlockX, 
						finalSampleBlockY, 
						finalSampleBlockZ
				), 
				coverage,
				allTerrain,
				surfaces,
				pointers
		);
	}
}
